package com.lh.project.common;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lh.project.model.enums.SystemConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.function.Function;

@Slf4j
@Component
public class CacheClient {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     *
     * @param id 表示要对象东西的Id
     * @param keyPrefix 表示该对象在Redis中存储的键的前缀
     * @param clazz 表示要查询对象的类
     * @param dbFallBack 该类Service层所对应的查询方法
     * @param time 表示过期时间
     * @param unit 过期时间的单位
     * @param <T>
     * @return
     */
    public <T,ID> T queryWithPassThrough(ID id, String keyPrefix, Class<T> clazz, Function<ID,T> dbFallBack, long time, TimeUnit unit) {
        String key = keyPrefix+id;
        String json = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(json)){
            //表示在Redis中查询出来了数据，直接返回即可
            return BeanUtil.toBean(json,clazz);
        }
        //todo 重点标注：这一丢丢就是缓存击穿的精华！也就是缓存击穿的解决方案
        if(json!=null){
            //表示我们在redis中存储的是""，空值~
            return null;
        }
        //下面表示我们在redis中没有缓存过该对象，或者该对象已经过期；查询数据库
        T t = dbFallBack.apply(id);
        //todo 发生缓存击穿
        if(t == null){
            //发生了一次缓存击穿
            //todo 这里设置的过期时间间隔应该短一点
            stringRedisTemplate.opsForValue().set(key,"",time,unit);
            return null;
        }
        this.set(key,t,time,unit);
        return t;
    }

    /**
     * 像Redis中设置要缓存的数据
     * @param key
     * @param t 要缓存的对象
     * @param time 过期时间
     * @param unit 过期时间单位
     */
    private void set(String key, Object t, long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(t),time,unit);
    }

    /**
     * 获取锁:10秒之后自动删除锁
     * @param key 键
     * @param id 值
     * @param <ID>
     * @return
     */
    private <ID> boolean tryLock(String key,ID id){
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, id.toString(),10,TimeUnit.SECONDS);
        return BooleanUtil.isTrue(aBoolean);
    }

    /**
     * 删除锁
     * @param key
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

    /**
     * 线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(1);


    /**
     *
     * @param id 类别号
     * @param current 需要的页数
     * @param keyPrefix 主键前键
     * @param dbFallBack 函数
     * @param clazz 类
     * @param eq 对比列
     * @param time 时间
     * @param unit 时间的单位
     * @param LOCK 锁
     * @param <T> 相应对象
     * @param <ID> 主键类型
     * @return
     */
    public<T,ID> List<T> queryWithLogicalExpireByHashMap(
            ID id,int current,
            String keyPrefix,
            Function<QueryWrapper, List<T>> dbFallBack,
            Class<T> clazz,
            String eq,
            long time,
            TimeUnit unit,
            String LOCK) {
        //使用HashMap数据结构存储
        log.info("请求数据：id="+id+";page="+current);
        String key = keyPrefix+id;
        String json =(String) stringRedisTemplate.opsForHash().get(key, current + "");
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        log.info("查出来的json:"+json);
//        if(StrUtil.isBlank(json)&&StrUtil.equals(json,"空")){
        if(StrUtil.isBlank(json)){
            //只是简单的判断json为空或者为""旧说他是缓存击穿，就有点……
            //可能发生了缓存击穿
            log.info("可能发生了缓存击穿");
            return null;
        }
        //todo 那走到这的话，很可能出现redisData为Null的情况
        //todo 我们什么时候应该查询数据库呢：1. json为null的时候 2. redisData的ExpireTime过期的时候
        //todo 也就是当json不为空，且redisData没有过期的时候不用查
        LocalDateTime expireTime = redisData.getExpireTime();
        log.info("先查询redis："+redisData);
        if(json!=null && expireTime.isAfter(LocalDateTime.now())){
            log.info("页面没有过期，正常返回");
            JSONArray data =(JSONArray)redisData.getData();
            return JSONUtil.toList(data,clazz);
        }
        //下面开始表示过期了或者是为null了，就得重新查询数据库，进行分段的处理
        String lock = LOCK+id+":"+current;
        boolean isGet = tryLock(lock, current);
        if(isGet){
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    QueryWrapper<T> q = new QueryWrapper<>();
                    q.eq(eq, id);
                    List<T> list = dbFallBack.apply(q);
                    //TODO 发生缓存击穿：1. 你查的id就不存在，2. 你查的页面不存在（current太大了）
                    if (list == null) {
                        //发生缓存击穿
                        log.info("发生了缓存击穿");
                        stringRedisTemplate.opsForHash().put(key,current,"空");
                    }
//                    if(list.size()<current*SystemConstants.DEFAULT_PAGE_SIZE){
//                        //肯定是json为空过来的，而且数据库中没有,那就直接不用干下面的了
//                        stringRedisTemplate.opsForHash().put(key,current+"","空");
//                        return;
//                    }
                    //分段处理
                    int pageIndex = 1;//第pageIndex页
                    while (true) {
                        List<T> page = getPageList(pageIndex, list);
                        if (page == null) {
                            break;
                        }
                        RedisData<T> redisData1 = new RedisData<>();
                        redisData1.setData(page);
                        redisData1.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
                        stringRedisTemplate.opsForHash().put(key, pageIndex+"", JSONUtil.toJsonStr(redisData1));
                        pageIndex++;
                    }
                }catch (Exception e){
                    throw  new RuntimeException("查询数据库失败");
                }finally {
                    unLock(lock);
                }
            });
        }

        //重新获取缓存
        String data = (String)stringRedisTemplate.opsForHash().get(key, current+"");
        RedisData redisData1 = JSONUtil.toBean(data, RedisData.class);
        JSONArray data1 =(JSONArray)redisData1.getData();
        log.info("可能获取到旧值："+data1);
        return JSONUtil.toList(data1,clazz);
    }



    /**
     * 获取redis中的（逻辑过期）存储的数据结构是string
     * @param id 匹配的值
     * @param current 获取第几页
     * @param dbFallBack 调用函数
     * @param clazz 对应类型
     * @param eq 匹配的列
     * @param <T> 返回值
     * @param <ID> 匹配的值
     * @param time 和下面的构成过期时间
     * @param unit
     * @param LOCK 表示缓存对象实现逻辑删除锁对应的锁的前缀
     * @return
     */
    public <T,ID> List<T> queryWithLogicalExpireByString(ID id, int current,String keyPrefix,
           Function<QueryWrapper, List<T>> dbFallBack,
            Class<T> clazz,
            String eq,
            long time,
            TimeUnit unit,
            String LOCK) {
        String key = keyPrefix+id;
        //先查询redis
        log.info(id+"类型的"+current+"页");
        String json = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank(json)){
            log.info("逻辑过期，可能发生缓存击穿："+json);
            return null;
        }
        //非空且不为""
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        JSONArray data =(JSONArray) redisData.getData();
        log.info("逻辑过期，第一次查询："+data.toString());
        if(expireTime.isAfter(LocalDateTime.now())){
            //表示没有过期
            log.info("逻辑过期：还没有过期");
            return getPageList(current, JSONUtil.toList(data, clazz));
        }
        String lock = LOCK+id;
        //下面的表示过期了，查询数据库
        boolean isGet = tryLock(lock, id);
        if(isGet) {
            //表示获取到锁了，开启子线程查询数据库
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    QueryWrapper<T> q = new QueryWrapper<>();
                    q.eq(eq, id);
                    List<T> list = dbFallBack.apply(q);
                    log.info("过期查询数据库："+list);
                    if (list == null) {
                        //如果查询出来的结果为null，那么可能发生了缓存击穿
                        log.info("发生缓存击穿");
                        stringRedisTemplate.opsForValue().set(key, "", 20L, TimeUnit.SECONDS);
                    }
                    LocalDateTime expireTimeNew = LocalDateTime.now();
                    //因为考虑到缓存击穿的缘故，所以在这设加的时间为秒或者天
                    if (unit.equals(TimeUnit.SECONDS)) {
                        expireTime.plusSeconds(unit.toSeconds(time));
                    } else {
                        expireTime.plusDays(unit.toDays(time));
                    }
                    RedisData<T> tRedisData = new RedisData<>();
                    tRedisData.setExpireTime(expireTimeNew);
                    tRedisData.setData(list);
                    log.info("新查出来的RedisData:"+ JSONUtil.toJsonStr(tRedisData));
                    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(tRedisData));
                } catch (Exception e) {
                    throw new RuntimeException("缓存穿透查询数据库出错");
                } finally {
                    unLock(lock);
                }
            });

        }
        //所有线程重新获取缓存
        String res = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank(res)){
            //说明可能出现了缓存击穿
            return null;
        }
        RedisData redisData1 = JSONUtil.toBean(res, RedisData.class);
        JSONArray data1 =(JSONArray) redisData1.getData();
        log.info("获取锁失败查询："+StrUtil.toString(data1));
        return getPageList(current, JSONUtil.toList(data1, clazz));
    }


    private <T> List<T> getPageList(int current,List<T> list){
        if(list==null){
            return null;
        }
        int start = (current-1)* SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current*SystemConstants.DEFAULT_PAGE_SIZE;
        if(end <= list.size()){
            log.info("start"+start+";end"+end+":正常");
            list = list.subList(start, end);
        }else if(end > list.size() && start<list.size()){
            list = list.subList(start,list.size());
            log.info("start"+start+";end"+end+":超了一点点");
        }else{
            //那就是start也越界了
            log.info("start"+start+";end"+end+":越界");
            list = null;
        }
        return list;
    }





    //只为纪念
    /**
     * 获取redis中的（逻辑过期）
     * @param id 匹配的值
     * @param current 获取第几页
     * @param dbFallBack 调用函数
     * @param clazz 对应类型
     * @param eq 匹配的列
     * @param <T> 返回值
     * @param <ID> 匹配的值
     * @param time 和下面的构成过期时间
     * @param unit
     * @param LOCK 表示缓存对象实现逻辑删除锁对应的锁的前缀
     * @return
     */
    public <T,ID> List<T> queryWithLogicalExpireTmp(ID id, int current,String keyPrefix,
                                                 Function<QueryWrapper, List<T>> dbFallBack,
                                                 Class<T> clazz,
                                                 String eq,
                                                 long time,
                                                 TimeUnit unit,
                                                 String LOCK) {
        String key = keyPrefix+id;
        //先查询redis
        /*
        本来这个方法就很好用呀！分段返回，但是要设置过期时间，分段就不可以了！
        int start = (current-1)* SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current*SystemConstants.DEFAULT_PAGE_SIZE;
        String s = stringRedisTemplate.opsForValue().get(key, start, end);
         */
        log.info(id+"类型的"+current+"页");
        String json = stringRedisTemplate.opsForValue().get(key);
//        if(json!=null &&json.equals("")){
        if(StrUtil.isBlank(json)){
//        if(json!=null && json.equals("")){
            //当json为""说明，缓存中没有，那缓存中没有可能就出现了缓存击穿
            //有了这个缓存击穿手段，那么就注定了过期时间不可太长
            log.info("逻辑过期，可能发生缓存击穿："+json);
            return null;
        }
        //非空且不为""
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        JSONArray data =(JSONArray) redisData.getData();
        log.info("逻辑过期，第一次查询："+data.toString());
        if(expireTime.isAfter(LocalDateTime.now())){
            //表示没有过期
            log.info("逻辑过期：还没有过期");
            return getPageList(current, JSONUtil.toList(data, clazz));
        }
        String lock = LOCK+id;
        //下面的表示过期了，查询数据库
        boolean isGet = tryLock(lock, id);
        if(isGet) {
            //表示获取到锁了，开启子线程查询数据库
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    QueryWrapper<T> q = new QueryWrapper<>();
                    q.eq(eq, id);
                    List<T> list = dbFallBack.apply(q);
                    log.info("过期查询数据库："+list);
                    if (list == null) {
                        //如果查询出来的结果为null，那么可能发生了缓存击穿
                        log.info("发生缓存击穿");
                        stringRedisTemplate.opsForValue().set(key, "", 20L, TimeUnit.SECONDS);
                    }
                    LocalDateTime expireTimeNew = LocalDateTime.now();
                    //因为考虑到缓存击穿的缘故，所以在这设加的时间为秒或者天
                    //如果你要说这个不是有plus方法嘛？这个你可以自己试一下
                    if (unit.equals(TimeUnit.SECONDS)) {
                        expireTime.plusSeconds(unit.toSeconds(time));
                    } else {
                        expireTime.plusDays(unit.toDays(time));
                    }
                    RedisData<T> tRedisData = new RedisData<>();
                    tRedisData.setExpireTime(expireTimeNew);
                    tRedisData.setData(list);
                    log.info("新查出来的RedisData:"+ JSONUtil.toJsonStr(tRedisData));
                    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(tRedisData));
                } catch (Exception e) {
                    throw new RuntimeException("缓存穿透查询数据库出错");
                } finally {
//                    unLock(key);
                    unLock(lock);
                }
            });

        }

        //所有线程重新获取缓存
        String res = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank(res)){
            //说明可能出现了缓存击穿
            return null;
        }
        RedisData redisData1 = JSONUtil.toBean(res, RedisData.class);
        JSONArray data1 =(JSONArray) redisData1.getData();
        log.info("获取锁失败查询："+StrUtil.toString(data1));
        return getPageList(current, JSONUtil.toList(data1, clazz));
    }
}
